// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/app_list/views/app_list_folder_view.h"

#include <algorithm>

#include "ui/accessibility/ax_view_state.h"
#include "ui/app_list/app_list_constants.h"
#include "ui/app_list/app_list_folder_item.h"
#include "ui/app_list/app_list_model.h"
#include "ui/app_list/views/app_list_item_view.h"
#include "ui/app_list/views/app_list_main_view.h"
#include "ui/app_list/views/apps_container_view.h"
#include "ui/app_list/views/apps_grid_view.h"
#include "ui/app_list/views/contents_view.h"
#include "ui/app_list/views/folder_background_view.h"
#include "ui/app_list/views/folder_header_view.h"
#include "ui/app_list/views/search_box_view.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/events/event.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/view_model.h"
#include "ui/views/view_model_utils.h"

namespace app_list {

namespace {

    // Indexes of interesting views in ViewModel of AppListFolderView.
    const int kIndexFolderHeader = 0;
    const int kIndexChildItems = 1;

    // Threshold for the distance from the center of the item to the circle of the
    // folder container ink bubble, beyond which, the item is considered dragged
    // out of the folder boundary.
    const int kOutOfFolderContainerBubbleDelta = 30;

} // namespace

AppListFolderView::AppListFolderView(AppsContainerView* container_view,
    AppListModel* model,
    AppListMainView* app_list_main_view)
    : container_view_(container_view)
    , app_list_main_view_(app_list_main_view)
    , folder_header_view_(new FolderHeaderView(this))
    , view_model_(new views::ViewModel)
    , model_(model)
    , folder_item_(NULL)
    , hide_for_reparent_(false)
{
    AddChildView(folder_header_view_);
    view_model_->Add(folder_header_view_, kIndexFolderHeader);

    items_grid_view_ = new AppsGridView(app_list_main_view_);
    items_grid_view_->set_folder_delegate(this);
    items_grid_view_->SetLayout(
        container_view->apps_grid_view()->cols(),
        container_view->apps_grid_view()->rows_per_page());
    items_grid_view_->SetModel(model);
    AddChildView(items_grid_view_);
    view_model_->Add(items_grid_view_, kIndexChildItems);

    SetPaintToLayer(true);
    SetFillsBoundsOpaquely(false);

    model_->AddObserver(this);
}

AppListFolderView::~AppListFolderView()
{
    model_->RemoveObserver(this);

    // This prevents the AppsGridView's destructor from calling the now-deleted
    // AppListFolderView's methods if a drag is in progress at the time.
    items_grid_view_->set_folder_delegate(nullptr);
}

void AppListFolderView::SetAppListFolderItem(AppListFolderItem* folder)
{
    accessible_name_ = ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
        IDS_APP_LIST_FOLDER_OPEN_FOLDER_ACCESSIBILE_NAME);
    NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true);

    folder_item_ = folder;
    items_grid_view_->SetItemList(folder_item_->item_list());
    folder_header_view_->SetFolderItem(folder_item_);
}

void AppListFolderView::ScheduleShowHideAnimation(bool show,
    bool hide_for_reparent)
{
    hide_for_reparent_ = hide_for_reparent;

    // Stop any previous animation.
    layer()->GetAnimator()->StopAnimating();

    // Hide the top items temporarily if showing the view for opening the folder.
    if (show)
        items_grid_view_->SetTopItemViewsVisible(false);

    // Set initial state.
    layer()->SetOpacity(show ? 0.0f : 1.0f);
    SetVisible(true);
    UpdateFolderNameVisibility(true);

    ui::ScopedLayerAnimationSettings animation(layer()->GetAnimator());
    animation.SetTweenType(
        show ? kFolderFadeInTweenType : kFolderFadeOutTweenType);
    animation.AddObserver(this);
    animation.SetTransitionDuration(base::TimeDelta::FromMilliseconds(
        show ? kFolderTransitionInDurationMs : kFolderTransitionOutDurationMs));

    layer()->SetOpacity(show ? 1.0f : 0.0f);
}

gfx::Size AppListFolderView::GetPreferredSize() const
{
    const gfx::Size header_size = folder_header_view_->GetPreferredSize();
    const gfx::Size grid_size = items_grid_view_->GetPreferredSize();
    int width = std::max(header_size.width(), grid_size.width());
    int height = header_size.height() + grid_size.height();
    return gfx::Size(width, height);
}

void AppListFolderView::Layout()
{
    CalculateIdealBounds();
    views::ViewModelUtils::SetViewBoundsToIdealBounds(*view_model_);
}

bool AppListFolderView::OnKeyPressed(const ui::KeyEvent& event)
{
    // Process TAB if focus should go to header; otherwise, AppsGridView will do
    // the right thing.
    if (event.key_code() == ui::VKEY_TAB) {
        if (items_grid_view_->has_selected_view() == event.IsShiftDown() && !folder_header_view_->HasTextFocus()) {
            folder_header_view_->SetTextFocus();
            items_grid_view_->ClearAnySelectedView();
            return true;
        } else {
            GiveBackFocusToSearchBox();
        }
    }

    // This will select an app in the list, so we need to relinquish focus.
    if (event.key_code() == ui::VKEY_DOWN)
        GiveBackFocusToSearchBox();

    return items_grid_view_->OnKeyPressed(event);
}

void AppListFolderView::OnAppListItemWillBeDeleted(AppListItem* item)
{
    if (item == folder_item_) {
        items_grid_view_->OnFolderItemRemoved();
        folder_header_view_->OnFolderItemRemoved();
        folder_item_ = NULL;

        // Do not change state if it is hidden.
        if (hide_for_reparent_ || layer()->opacity() == 0.0f)
            return;

        // If the folder item associated with this view is removed from the model,
        // (e.g. the last item in the folder was deleted), reset the view and signal
        // the container view to show the app list instead.
        // Pass NULL to ShowApps() to avoid triggering animation from the deleted
        // folder.
        container_view_->ShowApps(NULL);
    }
}

void AppListFolderView::OnImplicitAnimationsCompleted()
{
    // Show the top items when the opening folder animation is done.
    if (layer()->opacity() == 1.0f)
        items_grid_view_->SetTopItemViewsVisible(true);

    // If the view is hidden for reparenting a folder item, it has to be visible,
    // so that drag_view_ can keep receiving mouse events.
    if (layer()->opacity() == 0.0f && !hide_for_reparent_)
        SetVisible(false);

    // Set the view bounds to a small rect, so that it won't overlap the root
    // level apps grid view during folder item reprenting transitional period.
    if (hide_for_reparent_)
        SetBoundsRect(gfx::Rect(bounds().x(), bounds().y(), 1, 1));
}

void AppListFolderView::CalculateIdealBounds()
{
    gfx::Rect rect(GetContentsBounds());
    if (rect.IsEmpty())
        return;

    gfx::Rect header_frame(rect);
    gfx::Size size = folder_header_view_->GetPreferredSize();
    header_frame.set_height(size.height());
    view_model_->set_ideal_bounds(kIndexFolderHeader, header_frame);

    gfx::Rect grid_frame(rect);
    grid_frame.Subtract(header_frame);
    view_model_->set_ideal_bounds(kIndexChildItems, grid_frame);
}

void AppListFolderView::StartSetupDragInRootLevelAppsGridView(
    AppListItemView* original_drag_view,
    const gfx::Point& drag_point_in_root_grid,
    bool has_native_drag)
{
    // Converts the original_drag_view's bounds to the coordinate system of
    // root level grid view.
    gfx::RectF rect_f(original_drag_view->bounds());
    views::View::ConvertRectToTarget(items_grid_view_,
        container_view_->apps_grid_view(),
        &rect_f);
    gfx::Rect rect_in_root_grid_view = gfx::ToEnclosingRect(rect_f);

    container_view_->apps_grid_view()
        ->InitiateDragFromReparentItemInRootLevelGridView(original_drag_view,
            rect_in_root_grid_view,
            drag_point_in_root_grid,
            has_native_drag);
}

gfx::Rect AppListFolderView::GetItemIconBoundsAt(int index)
{
    AppListItemView* item_view = items_grid_view_->GetItemViewAt(index);
    // Icon bounds relative to AppListItemView.
    const gfx::Rect icon_bounds = item_view->GetIconBounds();
    gfx::Rect to_apps_grid_view = item_view->ConvertRectToParent(icon_bounds);
    gfx::Rect to_folder = items_grid_view_->ConvertRectToParent(to_apps_grid_view);

    // Get the icon image's bound.
    to_folder.ClampToCenteredSize(
        gfx::Size(kGridIconDimension, kGridIconDimension));

    return to_folder;
}

void AppListFolderView::UpdateFolderViewBackground(bool show_bubble)
{
    if (hide_for_reparent_)
        return;

    // Before showing the folder container inking bubble, hide the folder name.
    if (show_bubble)
        UpdateFolderNameVisibility(false);

    container_view_->folder_background_view()->UpdateFolderContainerBubble(
        show_bubble ? FolderBackgroundView::SHOW_BUBBLE : FolderBackgroundView::HIDE_BUBBLE);
}

void AppListFolderView::UpdateFolderNameVisibility(bool visible)
{
    folder_header_view_->UpdateFolderNameVisibility(visible);
}

void AppListFolderView::SetBackButtonLabel(bool folder)
{
    app_list_main_view_->search_box_view()->SetBackButtonLabel(folder);
}

bool AppListFolderView::IsPointOutsideOfFolderBoundary(
    const gfx::Point& point)
{
    if (!GetLocalBounds().Contains(point))
        return true;

    gfx::Point center = GetLocalBounds().CenterPoint();
    float delta = (point - center).Length();
    return delta > container_view_->folder_background_view()->GetFolderContainerBubbleRadius() + kOutOfFolderContainerBubbleDelta;
}

// When user drags a folder item out of the folder boundary ink bubble, the
// folder view UI will be hidden, and switch back to top level AppsGridView.
// The dragged item will seamlessly move on the top level AppsGridView.
// In order to achieve the above, we keep the folder view and its child grid
// view visible with opacity 0, so that the drag_view_ on the hidden grid view
// will keep receiving mouse event. At the same time, we initiated a new
// drag_view_ in the top level grid view, and keep it moving with the hidden
// grid view's drag_view_, so that the dragged item can be engaged in drag and
// drop flow in the top level grid view. During the reparenting process, the
// drag_view_ in hidden grid view will dispatch the drag and drop event to
// the top level grid view, until the drag ends.
void AppListFolderView::ReparentItem(
    AppListItemView* original_drag_view,
    const gfx::Point& drag_point_in_folder_grid,
    bool has_native_drag)
{
    // Convert the drag point relative to the root level AppsGridView.
    gfx::Point to_root_level_grid = drag_point_in_folder_grid;
    ConvertPointToTarget(items_grid_view_,
        container_view_->apps_grid_view(),
        &to_root_level_grid);
    StartSetupDragInRootLevelAppsGridView(
        original_drag_view, to_root_level_grid, has_native_drag);
    container_view_->ReparentFolderItemTransit(folder_item_);
}

void AppListFolderView::DispatchDragEventForReparent(
    AppsGridView::Pointer pointer,
    const gfx::Point& drag_point_in_folder_grid)
{
    AppsGridView* root_grid = container_view_->apps_grid_view();
    gfx::Point drag_point_in_root_grid = drag_point_in_folder_grid;
    ConvertPointToTarget(items_grid_view_, root_grid, &drag_point_in_root_grid);
    root_grid->UpdateDragFromReparentItem(pointer, drag_point_in_folder_grid);
}

void AppListFolderView::DispatchEndDragEventForReparent(
    bool events_forwarded_to_drag_drop_host,
    bool cancel_drag)
{
    container_view_->apps_grid_view()->EndDragFromReparentItemInRootLevel(
        events_forwarded_to_drag_drop_host, cancel_drag);
    container_view_->ReparentDragEnded();
}

void AppListFolderView::HideViewImmediately()
{
    SetVisible(false);
    hide_for_reparent_ = false;
}

void AppListFolderView::CloseFolderPage()
{
    accessible_name_ = ui::ResourceBundle::GetSharedInstance().GetLocalizedString(
        IDS_APP_LIST_FOLDER_CLOSE_FOLDER_ACCESSIBILE_NAME);
    NotifyAccessibilityEvent(ui::AX_EVENT_ALERT, true);

    GiveBackFocusToSearchBox();
    if (items_grid_view()->dragging())
        items_grid_view()->EndDrag(true);
    items_grid_view()->ClearAnySelectedView();
    container_view_->ShowApps(folder_item_);
}

bool AppListFolderView::IsOEMFolder() const
{
    return folder_item_->folder_type() == AppListFolderItem::FOLDER_TYPE_OEM;
}

void AppListFolderView::SetRootLevelDragViewVisible(bool visible)
{
    container_view_->apps_grid_view()->SetDragViewVisible(visible);
}

void AppListFolderView::GetAccessibleState(ui::AXViewState* state)
{
    state->role = ui::AX_ROLE_BUTTON;
    state->name = accessible_name_;
}

void AppListFolderView::NavigateBack(AppListFolderItem* item,
    const ui::Event& event_flags)
{
    CloseFolderPage();
}

void AppListFolderView::GiveBackFocusToSearchBox()
{
    app_list_main_view_->search_box_view()->search_box()->RequestFocus();
}

void AppListFolderView::SetItemName(AppListFolderItem* item,
    const std::string& name)
{
    model_->SetItemName(item, name);
}

} // namespace app_list
